// Author (file owner): Marc Belmont <code marcbelmont.com>
// Contributors: Shaun Siau Kai-Wei <gandalfmuaddib gmail.com>
// Contributors: David Bolton <davidkbolton gmail.com>
// Contributors: Flavien Dubus <flavien.dubus gmail.com>

//////////////////////////
// Class implementation //
//////////////////////////

function Functions(calc) {
  this.init(calc);
}

Functions.prototype = {
 calculator : null,
 conversions : null,
 _angle : null,
 mBranch : null,

 init : function(calc) {
    this.calculator = calc;
    this.conversions = new Conversions();
    var prefService = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService);
    this.mBranch = prefService.getBranch("calculator.");
    this.getAngle();
  },

  /////////////////////////
  // Setters and getters //
  /////////////////////////

  getAngle : function() {
    this._angle = this.getPreference(PREF_NAME_ANGLE, "Int", RADIAN);
  },

  getPreference : function(aPref, aType, aDefault) {
    try {
      var val = eval("this.mBranch.get" + aType + "Pref('" + aPref + "')");
      return val;
    } catch(e) {
      this.setPreference(aPref, aType, aDefault);
      return aDefault;
    }
    return null;
  },
  setPreference : function(aPref, aType, aValue) {
    try {
      eval("this.mBranch.set" + aType + "Pref('" + aPref + "'," + aValue + ")");
      return true;
    } catch(e) {
      alert(e);
    }
    return false;
  },

  //////////////
  // Commands //
  //////////////

  selectFunction : function(expr) {
    this.getAngle();
    var func = "";
    var i;
    for (i = 0; i < expr.length; i++) {
      if (expr[i] != '(') {
	func = func.concat(expr[i]);
      }
      else
	break;
    }
    expr = expr.replace(new RegExp(func, "i"), "");
    var params = this.getParams(expr, func);
    var res = this.javascriptFunctions(func, params);
    if (res == WRONG_FUNC) {
      res = this.otherFunctions(func, params);
    }
    if (res == WRONG_FUNC) {
      res = this.conversions.selectFunction(func, params);
    }
    return res;
  },

  // Turns (5,2,4) into [5,2,4]
  getParams : function(expr, func) {
    var params = new Array();
    var temp = "";
    var i;
    var par = 0;
    for (i = 1; i < expr.length - 1; i++) {
      // test for ( and ) in case of func(func(x,y)) kind of expressions...
      if (expr[i] == '(') {
	par++;
      }
      else if (expr[i] == ')') {
	par--;
      }
      if (expr[i] != ',' || par != 0) {
	temp = temp.concat(expr[i]);
      }
      else {
	params = params.concat(temp);
	temp = "";
      }
    }
    params = params.concat(temp);
    var re = new RegExp("\\(", "gi");
    for (i = 0; i < params.length; i++) {
      // leading zero means the base is octal
      params[i] = this.StripZeros(params[i]);
      if (re.test(params[i]))
	params[i] = this.calculator.findFunction(params[i]);
      else if (func == "base16to")
	params[i] = params[i];
      else
	params[i] = eval(params[i]);
    }
    return params;
  },

 StripZeros : function(a) {
    for (var i = 0; i < a.length - 1; i++)
      if (a[i] != '0')
	break;
    return a.substr(i);
  },

 //Converts from the user-defined trigo mode to radians.
 angleConvTo : function(numb) {
    if (this._angle == DEGREE) {
      numb = numb * Math.PI / 180;
      return numb.toString(BASE);
    } else if (this._angle == GRAD) {
      numb = numb * 2 * Math.PI / 400;
      return numb.toString(BASE);
    }
    return numb.toString(BASE);
  },

 //Converts from radians to user-defined trigo mode.
 angleConvFrom : function(numb) {
    if (this._angle == DEGREE) {
      numb = numb / Math.PI * 180;
      return numb.toString(BASE);
    }
    else if (this._angle == GRAD) {
      numb = numb / 2 / Math.PI * 400;
      return numb.toString(BASE);
    }
    return numb.toString(BASE);
  },

 javascriptFunctions : function(func, params) {
    var p0 = params[0];
    try {
      //Trigo
      if (func == "sin") {
	return Math.sin(this.angleConvTo(params[0])).toString(BASE);
      } else if (func == "cos") {
	return Math.cos(this.angleConvTo(params[0])).toString(BASE);
      } else if (func == "tan") {
	return Math.tan(this.angleConvTo(params[0])).toString(BASE);
      } else if (func == "asin") {
	return this.angleConvFrom(Math.asin(params[0])).toString(BASE);
      } else if (func == "acos") {
	return this.angleConvFrom(Math.acos(params[0])).toString(BASE);
      } else if (func == "atan") {
	return this.angleConvFrom(Math.atan(params[0])).toString(BASE);
      } else if (func == "atan2") {
	return this.angleConvFrom(Math.atan2(params[0], params[1])).toString(BASE);

	// Others...
      } else if (func == "abs") {
	return Math.abs(params[0]).toString(BASE);
      } else if (func == "ceil") {
	return Math.ceil(params[0]).toString(BASE);
      } else if (func == "exp") {
	return Math.exp(params[0]).toString(BASE);
      } else if (func == "floor") {
	return Math.floor(params[0]).toString(BASE);
      } else if (func == "log") {
	return Math.log(params[0]).toString(BASE);
      } else if (func == "max") {
	return Math.max(params[0], params[1]).toString(BASE);
      } else if (func == "min") {
	return Math.min(params[0], params[1]).toString(BASE);
      } else if (func == "pow") {
	return Math.pow(params[0], params[1]).toString(BASE);
      } else if (func == "random") {
	return Math.random().toString(BASE);
      } else if (func == "round") {
	return Math.round(params[0]).toString(BASE);
      } else if (func == "sqrt") {
	return Math.sqrt(params[0]).toString(BASE);


	//hyperbolic
      } else if (func == "sinh") {
	var num=(Math.exp(p0)-Math.exp(-p0))/2;
	return num.toString(BASE);
      } else if (func == "cosh") {
	var num=(Math.exp(p0)+Math.exp(-p0))/2;
	return num.toString(BASE);
      } else if (func == "tanh") {
	var num=(Math.exp(p0)-Math.exp(-p0))/(Math.exp(p0)+Math.exp(-p0));
	return num.toString(BASE);
      } else if (func == "asinh") {
	var p02=p0*p0;
	var asinh=Math.log(p0+Math.sqrt(p02+1));
	return asinh.toString(BASE);
      } else if (func == "acosh") {
	var p02=p0*p0;
	var acosh=Math.log(p0+Math.sqrt(p02-1));
	return acosh.toString(BASE);
      } else if (func == "atanh") {
	var num=(1+p0);
	var den=(1-p0);
	var atanh=Math.log(num/den)/2;
	return atanh.toString(BASE);

      }

    }catch (e) {    }

    return WRONG_FUNC;

  },

  otherFunctions : function(func, params) {
    if (func == "fact") {
      return this.fact(params[0]);
    } else if (func == "log10") {
      return (Math.log(params[0]) / Math.log(10)).toString(BASE);
    } else {
      return WRONG_FUNC;
    }
  },

  //////////
  // Fact //
  //////////

 fact : function(number) {
    var num = eval(number);
    var res = new Number("1");
    // if (typeof num == "undefined")
    // num = 0;

    // Calculate positive integer factorials
    if (num >= 0 && num == parseInt(number)) {
      var i;
      for (i = num; i > 1; i--) {
	res *= i;
      }

      // Undefined for negative integers
    } else if (num <0 && num == parseInt(number)) {
      return WRONG_FUNC;

      // Calculate non-integer factorials
    } else {
      g = 7;
      // Coefficients used by the GNU Scientific Library
      p = [0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313, -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7];
      res = this.gamma(num+1,g,p);
    }
    return res.toString(BASE);
  },


  // Gamma function based on sample script at: http://en.wikipedia.org/wiki/Lanczos_approximation
  gamma : function(z,g,p) {
    // Reflection formula
    if (z < 0.5) {
      return Math.PI / (Math.sin(Math.PI*z)*gamma(1-z,g,p));
    } else {
      z -= 1;
      x = p[0];
      for (i=1; i <= g+1; i++) {
	x += p[i]/(z+i);
      }
      t = z + g + 0.5;
      return Math.sqrt(2*Math.PI) * Math.pow(t,z+0.5) * Math.exp(-t) * x;
    }
  }
}

